// ---------------------------------------------------------------------------------------------
// <copyright file="Parser.cs" company="Swyx Solutions AG">
//     (c) Swyx Solutions AG
//     Original code (c) Ray Hayes (http://www.codeproject.com/KB/recipes/commandlineparser.aspx),
//     licensed under The code project open license (http://www.codeproject.com/info/cpol10.aspx)
// </copyright>
// <summary>Type ImportRR which implements SwyxWare routing record import</summary>
// ---------------------------------------------------------------------------------------------
namespace RJH.CommandLineHelper
{
    using System;
    using System.Runtime.InteropServices;
    using System.Text.RegularExpressions;

    /// <summary>Implementation of a command-line parsing class.  Is capable of
    /// having switches registered with it directly or can examine a registered
    /// class for any properties with the appropriate attributes appended to
    /// them.</summary>
    [ComVisible(false)]
    public class Parser
    {
        #region Private Variables
        /// <summary>
        /// command line to parse
        /// </summary>
        private string commandLine = string.Empty;

        /// <summary>
        /// working string used during parsing
        /// </summary>
        private string workingString = string.Empty;

        /// <summary>
        /// application name
        /// </summary>
        private string applicationName = string.Empty;

        /// <summary>
        /// split parameters
        /// </summary>
        private string[] splitParameters = null;

        /// <summary>
        /// command line switches
        /// </summary>
        private System.Collections.ArrayList switches = null;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Parser class.
        /// </summary>
        /// <param name="commandLine">command line to parse</param>
        public Parser(string commandLine)
        {
            this.commandLine = commandLine;
        }

        /// <summary>
        /// Initializes a new instance of the Parser class.
        /// </summary>
        /// <param name="commandLine">command line to parse</param>
        /// <param name="classForAutoAttributes">object to retrieve command line auto-attributes from</param>
        public Parser(string commandLine, object classForAutoAttributes)
        {
            this.commandLine = commandLine;

            Type type = classForAutoAttributes.GetType();
            System.Reflection.MemberInfo[] members = type.GetMembers();

            for (int i = 0; i < members.Length; i++)
            {
                object[] attributes = members[i].GetCustomAttributes(false);
                if (attributes.Length > 0)
                {
                    SwitchRecord rec = null;

                    foreach (Attribute attribute in attributes)
                    {
                        if (attribute is CommandLineSwitchAttribute)
                        {
                            CommandLineSwitchAttribute switchAttrib =
                                (CommandLineSwitchAttribute)attribute;

                            // Get the property information.  We're only handling
                            // properties at the moment!
                            if (members[i] is System.Reflection.PropertyInfo)
                            {
                                System.Reflection.PropertyInfo pi = (System.Reflection.PropertyInfo)members[i];

                                rec = new SwitchRecord(
                                    switchAttrib.Name,
                                    switchAttrib.Parameter,
                                    switchAttrib.Description,
                                    pi.PropertyType);

                                // Map in the Get/Set methods.
                                rec.SetMethod = pi.GetSetMethod();
                                rec.GetMethod = pi.GetGetMethod();
                                rec.PropertyOwner = classForAutoAttributes;

                                // Can only handle a single switch for each property
                                // (otherwise the parsing of aliases gets silly...)
                                break;
                            }
                        }
                    }

                    // See if any aliases are required.  We can only do this after
                    // a switch has been registered and the framework doesn't make
                    // any guarantees about the order of attributes, so we have to
                    // walk the collection a second time.
                    if (rec != null)
                    {
                        foreach (Attribute attribute in attributes)
                        {
                            if (attribute is CommandLineAliasAttribute)
                            {
                                CommandLineAliasAttribute aliasAttrib =
                                    (CommandLineAliasAttribute)attribute;
                                rec.AddAlias(aliasAttrib.Alias);
                            }
                        }
                    }

                    // Assuming we have a switch record (that may or may not have
                    // aliases), add it to the collection of switches.
                    if (rec != null)
                    {
                        if (this.switches == null)
                        {
                            this.switches = new System.Collections.ArrayList();
                        }
                        
                        this.switches.Add(rec);
                    }
                }
            }
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets application name as extracted from the command line
        /// </summary>
        public string ApplicationName
        {
            get
            { 
                return this.applicationName; 
            }
        }

        /// <summary>
        /// Gets splitted command line parameters
        /// </summary>
        public string[] Parameters
        {
            get 
            { 
                return this.splitParameters; 
            }
        }

        /// <summary>
        /// Gets command line switches
        /// </summary>
        public SwitchInfo[] Switches
        {
            get
            {
                if (this.switches == null)
                {
                    return null;
                }
                else
                {
                    SwitchInfo[] si = new SwitchInfo[this.switches.Count];
                    for (int i = 0; i < this.switches.Count; i++)
                    {
                        si[i] = new SwitchInfo(this.switches[i]);
                    }
                    
                    return si;
                }
            }
        }

        /// <summary>
        /// Gets a list of the unhandled switches
        /// that the parser has seen, but not processed.
        /// </summary>
        /// <remark>The unhandled switches are not removed from the remainder
        /// of the command-line.</remark>
        public string[] UnhandledSwitches
        {
            get
            {
                string switchPattern = @"(\s|^)(?<match>(-{1,2}|/)(.+?))(?=(\s|$))";
                Regex r = new Regex(switchPattern, RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
                MatchCollection m = r.Matches(this.workingString);

                if (m != null)
                {
                    string[] unhandled = new string[m.Count];
                    for (int i = 0; i < m.Count; i++)
                    {
                        unhandled[i] = m[i].Groups["match"].Value;
                    }

                    return unhandled;
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets command line switch
        /// </summary>
        /// <param name="name">Name of switch to retrieve</param>
        /// <returns></returns>
        public object this[string name]
        {
            get
            {
                if (this.switches != null)
                {
                    for (int i = 0; i < this.switches.Count; i++)
                    {
                        if (string.Compare((this.switches[i] as SwitchRecord).Name, name, true) == 0)
                        {
                            return (this.switches[i] as SwitchRecord).Value;
                        }
                    }
                }

                return null;
            }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Add command line switch
        /// </summary>
        /// <param name="name">command line switch name</param>
        /// <param name="description">command line switch description</param>
        public void AddSwitch(string name, string description)
        {
            if (this.switches == null)
            {
                this.switches = new System.Collections.ArrayList();
            }

            SwitchRecord rec = new SwitchRecord(name, description);
            this.switches.Add(rec);
        }

        /// <summary>
        /// Add command line switch
        /// </summary>
        /// <param name="names">command line switch name</param>
        /// <param name="description">command line switch description</param>
        public void AddSwitch(string[] names, string description)
        {
            if (this.switches == null)
            {
                this.switches = new System.Collections.ArrayList();
            }
            
            SwitchRecord rec = new SwitchRecord(names[0], description);
            for (int s = 1; s < names.Length; s++)
            {
                rec.AddAlias(names[s]);
            }
            
            this.switches.Add(rec);
        }

        /// <summary>
        /// Parse command line
        /// </summary>
        /// <returns>true if parsing was successful</returns>
        public bool Parse()
        {
            this.ExtractApplicationName();

            // Remove switches and associated info.
            this.HandleSwitches();

            // Split parameters.
            this.SplitParameters();

            return true;
        }

        /// <summary>
        /// returns internal value of switch with given name
        /// </summary>
        /// <param name="name">switch name</param>
        /// <returns>switch's internal value</returns>
        public object InternalValue(string name)
        {
            if (this.switches != null)
            {
                for (int i = 0; i < this.switches.Count; i++)
                {
                    if (string.Compare((this.switches[i] as SwitchRecord).Name, name, true) == 0)
                    {
                        return (this.switches[i] as SwitchRecord).InternalValue;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// show command line help
        /// </summary>
        /// <param name="header">header to show before command line help</param>
        public void ShowHelp(string header)
        {
            int maxLength = 0;

            // find the longest string
            for (int i = 0; i < this.Switches.Length; i++)
            {
                int length = this.Switches[i].Name.Length + this.Switches[i].Parameter.Length;
                if (maxLength < length)
                {
                    maxLength = length;
                }
            }

            // Print out the strings
            Console.WriteLine(Console.Title);
            Console.WriteLine("{0}\n", header);

            string format = @"  -{0} {1,-" + maxLength.ToString() + @"} {2}";
            for (int i = 0; i < this.Switches.Length; i++)
            {
                Console.WriteLine(format, this.Switches[i].Name, this.Switches[i].Parameter, this.Switches[i].Description);
            }

            Console.WriteLine();
        }
        #endregion

        #region Private Utility Functions
        /// <summary>
        /// Extraxt application name from command line
        /// </summary>
        private void ExtractApplicationName()
        {
            Regex r = new Regex(@"^(?<commandLine>(""[^\""]+""|(\S)+))(?<remainder>.+)", RegexOptions.ExplicitCapture);
            Match m = r.Match(this.commandLine);
            if (m != null && m.Groups["commandLine"] != null)
            {
                this.applicationName = m.Groups["commandLine"].Value;
                this.workingString = m.Groups["remainder"].Value;
            }
        }

        /// <summary>
        /// split command line parameters
        /// </summary>
        private void SplitParameters()
        {
            // Populate the split parameters array with the remaining parameters.
            // Note that if quotes are used, the quotes are removed.
            // e.g.   one two three "four five six"
            // 0 - one
            // 1 - two
            // 2 - three
            // 3 - four five six
            // (e.g. 3 is not in quotes).
            Regex r = new Regex(@"((\s*(""(?<param>.+?)""|(?<param>\S+))))", RegexOptions.ExplicitCapture);
            MatchCollection m = r.Matches(this.workingString);

            if (m != null)
            {
                this.splitParameters = new string[m.Count];
                for (int i = 0; i < m.Count; i++)
                {
                    this.splitParameters[i] = m[i].Groups["param"].Value;
                }
            }
        }

        /// <summary>
        /// find and command line switches and extract values
        /// </summary>
        private void HandleSwitches()
        {
            if (this.switches != null)
            {
                foreach (SwitchRecord s in this.switches)
                {
                    Regex r = new Regex(
                        s.Pattern,
                        RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);

                    MatchCollection m = r.Matches(this.workingString);
                    if (m != null)
                    {
                        for (int i = 0; i < m.Count; i++)
                        {
                            string value = null;
                            if (m[i].Groups != null && m[i].Groups["value"] != null)
                            {
                                value = m[i].Groups["value"].Value;
                            }

                            if (s.Type == typeof(bool))
                            {
                                bool state = true;

                                // The value string may indicate what value we want.
                                if (m[i].Groups != null && m[i].Groups["value"] != null)
                                {
                                    switch (value)
                                    {
                                        case "+": state = true;
                                            break;
                                        case "-": state = false;
                                            break;
                                        case "":
                                            if (s.ReadValue != null)
                                            {
                                                state = !(bool)s.ReadValue;
                                            }

                                            break;
                                        default: break;
                                    }
                                }

                                s.Notify(state);
                                break;
                            }
                            else if (s.Type == typeof(string))
                            {
                                s.Notify(value);
                            }
                            else if (s.Type == typeof(int))
                            {
                                s.Notify(int.Parse(value));
                            }
                            else if (s.Type.IsEnum)
                            {
                                s.Notify(System.Enum.Parse(s.Type, value, true));
                            }
                        }
                    }

                    this.workingString = r.Replace(this.workingString, " ");
                }
            }
        }
        #endregion

        /// <summary>A simple internal class for passing back to the caller
        /// some information about the switch.  The internals/implementation
        /// of this class has privillaged access to the contents of the
        /// SwitchRecord class.</summary>
        public class SwitchInfo
        {
            #region Private Variables
            /// <summary>
            /// switch record object
            /// </summary>
            private object switchObj = null;
            #endregion

            /// <summary>
            /// Initializes a new instance of the SwitchInfo class.  Note, in order to hide to the outside world
            /// information not necessary to know, the constructor takes a System.Object (aka
            /// object) as it's registering type.  If the type isn't of the correct type, an exception
            /// is thrown.
            /// </summary>
            /// <param name="rec">The SwitchRecord for which this class store information.</param>
            /// <exception cref="ArgumentException">Thrown if the rec parameter is not of
            /// the type SwitchRecord.</exception>
            public SwitchInfo(object rec)
            {
                if (rec is SwitchRecord)
                {
                    this.switchObj = rec;
                }
                else
                {
                    throw new ArgumentException();
                }
            }

            #region Public Properties
            /// <summary>
            /// Gets command line switch name
            /// </summary>
            public string Name
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Name;
                }
            }

            /// <summary>
            /// Gets command line switch paramter
            /// </summary>
            public string Parameter
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Parameter;
                }
            }

            /// <summary>
            /// Gets command line switch description
            /// </summary>
            public string Description
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Description;
                }
            }

            /// <summary>
            /// Gets command line switch aliases
            /// </summary>
            public string[] Aliases
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Aliases;
                }
            }

            /// <summary>
            /// Gets command line switch type
            /// </summary>
            public System.Type Type
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Type;
                }
            }

            /// <summary>
            /// Gets command line switch value
            /// </summary>
            public object Value
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Value;
                }
            }

            /// <summary>
            /// Gets command lie switch internal value
            /// </summary>
            public object InternalValue
            {
                get
                {
                    return (this.switchObj as SwitchRecord).InternalValue;
                }
            }

            /// <summary>
            /// Gets a value indicating whether command line switch type is an enumerator type
            /// </summary>
            public bool IsEnum
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Type.IsEnum;
                }
            }

            /// <summary>
            /// Gets command line switch enumerations. 
            /// </summary>
            public string[] Enumerations
            {
                get
                {
                    return (this.switchObj as SwitchRecord).Enumerations;
                }
            }
            #endregion
        }

        /// <summary>
        /// The SwitchRecord is stored within the parser's collection of registered
        /// switches.  This class is private to the outside world.
        /// </summary>
        private class SwitchRecord
        {
            #region Private Variables
            /// <summary>
            /// command line switch name
            /// </summary>
            private string name = string.Empty;

            /// <summary>
            /// command line switch parameter
            /// </summary>
            private string param = string.Empty;

            /// <summary>
            /// command line switch description
            /// </summary>
            private string description = string.Empty;

            /// <summary>
            /// command line switch value
            /// </summary>
            private object value = null;

            /// <summary>
            /// command line switch type
            /// </summary>
            private System.Type switchType = typeof(bool);

            /// <summary>
            /// command line switch aliases
            /// </summary>
            private System.Collections.ArrayList aliases = null;

            /// <summary>
            /// command line switch pattern
            /// </summary>
            private string pattern = string.Empty;

            /// <summary>
            /// The following advanced functions allow for callbacks to be
            /// made to manipulate the associated data type.
            /// </summary>
            private System.Reflection.MethodInfo setMethod = null;

            /// <summary>
            /// The following advanced functions allow for callbacks to be
            /// made to manipulate the associated data type.
            /// </summary>
            private System.Reflection.MethodInfo getMethod = null;

            /// <summary>
            /// property owner
            /// </summary>
            private object propertyOwner = null;
            #endregion

            #region Constructors
            /// <summary>
            /// Initializes a new instance of the SwitchRecord class.
            /// </summary>
            /// <param name="name">switch record name</param>
            /// <param name="description">switch record description</param>
            public SwitchRecord(string name, string description)
            {
                this.Initialize(name, description);
            }

            /// <summary>
            /// Initializes a new instance of the SwitchRecord class.
            /// </summary>
            /// <param name="name">switch record name</param>
            /// <param name="param">switch record parameter</param>
            /// <param name="description">switch record description</param>
            /// <param name="type">switch record type</param>
            public SwitchRecord(string name, string param, string description, System.Type type)
            {
                if (type == typeof(bool) ||
                     type == typeof(string) ||
                     type == typeof(int) ||
                     type.IsEnum)
                {
                    this.switchType = type;
                    this.Initialize(name, param, description);
                }
                else
                {
                    throw new ArgumentException("Currently only Ints, Bool and Strings are supported");
                }
            }
            #endregion

            #region Public Properties
            /// <summary>
            /// Gets switch record value
            /// </summary>
            public object Value
            {
                get
                {
                    if (this.ReadValue != null)
                    {
                        return this.ReadValue;
                    }
                    else
                    {
                        return this.value;
                    }
                }
            }

            /// <summary>
            /// Gets switch record internal value
            /// </summary>
            public object InternalValue
            {
                get
                {
                    return this.value;
                }
            }

            /// <summary>
            /// Gets or sets switch record name
            /// </summary>
            public string Name
            {
                get
                {
                    return this.name;
                }

                set
                {
                    this.name = value;
                }
            }

            /// <summary>
            /// Gets or sets switch record parameter
            /// </summary>
            public string Parameter
            {
                get
                {
                    return this.param;
                }

                set
                {
                    this.param = value;
                }
            }

            /// <summary>
            /// Gets or sets switch record description
            /// </summary>
            public string Description
            {
                get
                {
                    return this.description;
                }

                set
                {
                    this.description = value;
                }
            }

            /// <summary>
            /// Gets switch record type
            /// </summary>
            public System.Type Type
            {
                get
                {
                    return this.switchType;
                }
            }

            /// <summary>
            /// Gets switch record aliases
            /// </summary>
            public string[] Aliases
            {
                get
                {
                    return (this.aliases != null) ? (string[])this.aliases.ToArray(typeof(string)) : null;
                }
            }

            /// <summary>
            /// Gets switch record detection pattern
            /// </summary>
            public string Pattern
            {
                get { return this.pattern; }
            }

            /// <summary>
            /// Sets switch record custom SetMethod
            /// </summary>
            public System.Reflection.MethodInfo SetMethod
            {
                set
                {
                    this.setMethod = value;
                }
            }

            /// <summary>
            /// Sets switch record custom GetMethod
            /// </summary>
            public System.Reflection.MethodInfo GetMethod
            {
                set
                {
                    this.getMethod = value;
                }
            }

            /// <summary>
            /// Sets switch record property owner
            /// </summary>
            public object PropertyOwner
            {
                set
                {
                    this.propertyOwner = value;
                }
            }

            /// <summary>
            /// Gets property value via custom GetMethod
            /// </summary>
            public object ReadValue
            {
                get
                {
                    object o = null;
                    if (this.propertyOwner != null && this.getMethod != null)
                    {
                        o = this.getMethod.Invoke(this.propertyOwner, null);
                    }

                    return o;
                }
            }

            /// <summary>
            /// Gets switch record enumeration values
            /// </summary>
            public string[] Enumerations
            {
                get
                {
                    if (this.switchType.IsEnum)
                    {
                        return System.Enum.GetNames(this.switchType);
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            #endregion

            #region Public Methods
            /// <summary>
            /// Add switch record alias
            /// </summary>
            /// <param name="alias">Alias to add</param>
            public void AddAlias(string alias)
            {
                if (this.aliases == null)
                {
                    this.aliases = new System.Collections.ArrayList();
                }

                this.aliases.Add(alias);

                this.BuildPattern();
            }

            /// <summary>
            /// Set value by calling custom set method
            /// </summary>
            /// <param name="value">new property value</param>
            public void Notify(object value)
            {
                if (this.propertyOwner != null && this.setMethod != null)
                {
                    object[] parameters = new object[1];
                    parameters[0] = value;
                    this.setMethod.Invoke(this.propertyOwner, parameters);
                }

                this.value = value;
            }

            #endregion
            #region Private Utility Functions
            /// <summary>
            /// Initialize switch record object
            /// </summary>
            /// <param name="name">command line switch name</param>
            /// <param name="description">command line switch description</param>
            private void Initialize(string name, string description)
            {
                this.Initialize(name, string.Empty, description);
            }

            /// <summary>
            /// Initialize switch record object
            /// </summary>
            /// <param name="name">command line switch name</param>
            /// <param name="param">command line switch parameter</param>
            /// <param name="description">command line switch description</param>
            private void Initialize(string name, string param, string description)
            {
                this.name = name;
                this.param = param;
                this.description = description;

                this.BuildPattern();
            }

            /// <summary>
            /// build command line switch detection pattern
            /// </summary>
            private void BuildPattern()
            {
                string matchString = this.Name;
                if (matchString == "?")
                {
                    matchString = "\\?";
                }

                if (this.Aliases != null && this.Aliases.Length > 0)
                {
                    foreach (string s in this.Aliases)
                    {
                        matchString += "|" + s;
                    }
                }

                string strPatternStart = @"(\s|^)(?<match>(-{1,2}|/)(";

                string strPatternEnd;  // To be defined below.

                // The common suffix ensures that the switches are followed by
                // a white-space OR the end of the string. This will stop
                // switches such as /help matching /helpme
                string strCommonSuffix = @"""?(?=(\s|$))"; // @"(?=(\s|$))";

                if (this.Type == typeof(bool))
                {
                    strPatternEnd = @")(?<value>(\+|-){0,1}))";
                }
                else if (this.Type == typeof(string))
                {
                    // strPatternEnd = @")(?::|\s+))((?:"")(?<value>.+)(?:"")|(?<value>\S+))";
                    strPatternEnd = @")(?::|\s+))((?:"")(?<value>[^\""]+)(?:"")|(?<value>\S+))";
                }
                else if (this.Type == typeof(int))
                {
                    strPatternEnd = @")(?::|\s+))((?<value>(-|\+)[0-9]+)|(?<value>[0-9]+))";
                }
                else if (this.Type.IsEnum)
                {
                    string[] enumNames = this.Enumerations;
                    string enumStr = enumNames[0];
                    for (int e = 1; e < enumNames.Length; e++)
                    {
                        enumStr += "|" + enumNames[e];
                    }

                    strPatternEnd = @")(?::|\s+))(?<value>" + enumStr + @")";
                }
                else
                {
                    throw new System.ArgumentException();
                }

                // Set the internal regular expression pattern.
                this.pattern = strPatternStart + matchString + strPatternEnd + strCommonSuffix;
            }
            #endregion
        }
    }
}
